/*
 *    Copyright 2008 Tim Jansen
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package sg.atom.core.actor.internal;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import sg.atom.core.actor.Actor;
import sg.atom.core.actor.exceptions.ConfigurationException;
import sg.atom.core.annotations.Initializer;
import sg.atom.core.annotations.Message;
import sg.atom.core.bean.BeanClassDescriptor;
import sg.atom.utils.concurrent.AsyncResult;
import sg.atom.utils.event.message.MessageImplDescriptor;
import sg.atom.utils.concurrent.ConcurrencyModel;
import sg.atom.utils.concurrent.Model;
import sg.atom.utils.concurrent.Shared;
import sg.atom.utils.concurrent.ThreadSafe;
import sg.atom.utils.datastructure.collection.immutable.ImmutableHelper;

/**
 * Describes the configuration of an Actor sub-class. Immutable thread-safe.
 *
 * @see MessageImplDescriptor
 */
public final class ActorClassDescriptor {

    /**
     * The default concurrency model of the actor.
     */
    private final ConcurrencyModel concurrencyModel;
    /**
     * The message implementations of the actor class.
     */
    private final MessageImplDescriptor[] messages;
    /**
     * The {@link BeanClassDescriptor} of the actor.
     */
    private final BeanClassDescriptor beanClassDescriptor;

    /**
     * Creates a new instance.
     *
     * @param concurrencyModel the concurrency model used by the actor
     * @param msgDescriptors the message descriptors of the actor
     */
    private ActorClassDescriptor(ConcurrencyModel concurrencyModel, MessageImplDescriptor[] msgDescriptors, BeanClassDescriptor beanClassDescriptor) {
        this.concurrencyModel = concurrencyModel;
        this.messages = new MessageImplDescriptor[msgDescriptors.length];
        System.arraycopy(msgDescriptors, 0, this.messages, 0, msgDescriptors.length);
        this.beanClassDescriptor = beanClassDescriptor;
    }

    /**
     * Creates a new ActorClassDescriptor for the given actor class.
     *
     * @param actorClass the actor's class
     * @return the new instance
     * @throws ConfigurationException if the agent is not configured correctly
     */
    public static ActorClassDescriptor create(Class<? extends Actor> actorClass) {
        if (Modifier.isInterface(actorClass.getModifiers())) {
            throw new ConfigurationException(String.format("Actor class %s is an interface. You may implement an interface in an actor, but you can not instantiate it.", actorClass));
        }
        if (Modifier.isFinal(actorClass.getModifiers())) {
            throw new ConfigurationException(String.format("Actor class %s is final. Actors can not be final, because the Agent needs to create a sub-class of the Actor.", actorClass));
        }

        if ((actorClass.getDeclaringClass() != null) && !Modifier.isStatic(actorClass.getModifiers())) {
            throw new ConfigurationException(String.format("Actor class %s is a non-static inner class. If you declare an actor as inner class, you must use the 'static' modifier.", actorClass));
        }

        // check that there is a default constructor
        Constructor<?> ctor;
        try {
            ctor = actorClass.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            throw new ConfigurationException(String.format("Actor class %s does not have a default constructor (without arguments). "
                    + "A default constructor is required for every actor. If you need to initialize an Actor with arguments, use properties and override the init() method.", actorClass),
                    e);
        }

        if (Modifier.isPrivate(ctor.getModifiers())) {
            if (Modifier.isPrivate(actorClass.getModifiers())) {
                throw new ConfigurationException(String.format("Constructor of class %s must not be private. It is allowed to declare a private actor, but then you must define a non-private constructor without argument. Every actor needs a non-private default constructor.", actorClass));
            } else {
                throw new ConfigurationException(String.format("Constructor of class %s must not be private. Every actor needs a non-private default constructor.", actorClass));
            }
        }

        // evaluate @Model
        Model ac = actorClass.getAnnotation(Model.class);
        ConcurrencyModel actorConcurrencyModel = (ac != null) ? ac.value() : ConcurrencyModel.SingleThreaded;

        // find messages, undeclared msgs, and forbid other public messages
        ArrayList<Method> messages = new ArrayList<Method>();
        for (Method method : BeanClassDescriptor.getAllMethods(actorClass, Actor.class)) {
            int mods = method.getModifiers();
            if (method.getAnnotation(Message.class) != null) {
                messages.add(method);
            } else if (method.getReturnType().equals(AsyncResult.class)
                    && ((mods & (Modifier.PRIVATE | Modifier.FINAL | Modifier.STATIC)) == 0)) {
                throw new ConfigurationException(String.format("In Actor class %s, method %s returns AsyncResult, "
                        + "but has no @Message annotation. \n"
                        + "This is not allowed, in order to prevent methods that are accidentally not declared as messages. "
                        + "Please note that you also need to specify @Message if you are overriding "
                        + "an inherited method (Java annotations support the concept of inheritance only for classes, not "
                        + "for methods). If you are really sure that your method should not be a message, you "
                        + "must declare it either as 'private' or 'final'.",
                        actorClass.getName(), method.getName()));
            } else if (Modifier.isPublic(mods)
                    && (method.getAnnotation(ThreadSafe.class) == null)
                    && (method.getAnnotation(Initializer.class) == null)
                    && !Modifier.isAbstract(mods)
                    && !Modifier.isStatic(mods)) {
                throw new ConfigurationException(String.format("Method %s in actor %s is public,"
                        + "but neither a @Message, @Initializer, @Prop nor declared @ThreadSafe.\n"
                        + "This is not allowed. If you are sure that this method is thread-safe, "
                        + "add a @ThreadSafe. If you don't call this method from other classes, make "
                        + "it private or protected.",
                        method.getName(), actorClass.getName()));
            }
        }



        // create the instances
        MessageImplDescriptor[] msgDescriptors = new MessageImplDescriptor[messages.size()];
        for (int i = 0; i < msgDescriptors.length; i++) {
            msgDescriptors[i] = MessageImplDescriptor.createMessageDescriptor(actorClass, actorConcurrencyModel, messages.get(i));
        }

        BeanClassDescriptor bcd = BeanClassDescriptor.create(actorClass);

        // additional checks for stateless actors
        if (actorConcurrencyModel == ConcurrencyModel.Stateless) {
            checkStatelessActor(actorClass, bcd);
        }

        ActorClassDescriptor instance = new ActorClassDescriptor(actorConcurrencyModel,
                msgDescriptors, bcd);

        return instance;
    }

    /**
     * Checks that the given actor does not contain any state.
     *
     * @param actorClass the actor to check
     * @param bcd the BeanClassDescriptor of the actor
     * @throws ConfigurationException if the actor contains state
     */
    private static void checkStatelessActor(Class<? extends Actor> actorClass,
            BeanClassDescriptor bcd) {
        for (int i = 0; i < bcd.getPropertyCount(); i++) {
            PropertyDescriptor pd = bcd.getProperty(i);
            if (pd.getPropertySource().isGenerating()) {
                if (pd.getSetter() != null) {
                    throw new ConfigurationException(String.format("Property %s in actor %s (getter: %s) is not a read-only "
                            + "property, even though the actor is declared as stateless (@Model annotation). A stateless actor "
                            + "can only have read-only properties.",
                            pd.getName(), actorClass.getName(), pd.getGetter().getName()));
                }
                if (!(pd.isSharedReference() || isStatelessCompatible(pd.getPropertyClass()))) {
                    throw new ConfigurationException(String.format("Property %s in actor %s (getter: %s) has the non-stateless-compatible "
                            + "type %s without @Shared annotation, even though the actor is declared as stateless (@Model annotation). "
                            + "A stateless actor can only store immutable types, other actors and multi-threading-safe classes "
                            + "with @Shared annotation.",
                            pd.getName(), actorClass.getName(), pd.getGetter().getName(), pd.getPropertyClass().getName()));
                }
            }
        }

        checkStatelessFields(actorClass, actorClass);
    }

    /**
     * Checks that the given actor does not contain any state in its fields,
     * recursively going to super-classes.
     *
     * @param topActorClass the actor class that which contains actorClass and
     * it the actual class being checked (for error messages)
     * @param actorClass the actor class to check now
     */
    private static void checkStatelessFields(Class<? extends Actor> topActorClass, Class<?> actorClass) {
        for (Field f : actorClass.getDeclaredFields()) {
            if (!Modifier.isFinal(f.getModifiers())) {
                throw new ConfigurationException(String.format("Field %s in %s is not final, even though the actor %s is "
                        + "declared as stateless (@Model annotation). A stateless actor can only have final fields of "
                        + "immutable types or thread-safe types.",
                        f.getName(), actorClass.getName(), topActorClass.getName()));
            }
            if (!((f.getAnnotation(Shared.class) != null) || isStatelessCompatible(f.getType()))) {
                throw new ConfigurationException(String.format("Field %s in %s has the non-stateless-compatible "
                        + "type %s without @Shared annotation, even though the actor %s is declared as stateless (@Model annotation). "
                        + "A stateless actor can only store immutable types, other actors and multi-threading-safe classes "
                        + "with @Shared annotation.",
                        f.getName(), actorClass.getName(), f.getType().getName(), topActorClass.getName()));
            }
        }

        if (!actorClass.getSuperclass().equals(Actor.class)) {
            checkStatelessFields(topActorClass, actorClass.getSuperclass());
        }
    }

    /**
     * Returns the effective concurrency model of the Actor.
     *
     * @return the ConcurrencyModel
     */
    public ConcurrencyModel getConcurrencyModel() {
        return concurrencyModel;
    }

    /**
     * Returns the message at the given index.
     *
     * @param index the index of the message
     * @return the descriptor
     * @throws IndexOutOfBoundsException if there is no message at the given
     * index
     * @see #getMessageCount()
     */
    public MessageImplDescriptor getMessage(int index) {
        return messages[index];
    }

    /**
     * Returns the number of messages.
     *
     * @return the number of messages
     */
    public int getMessageCount() {
        return messages.length;
    }

    /**
     * Returns the actor's BeanClassDescriptor.
     *
     * @return the BeanClassDescriptor
     */
    public BeanClassDescriptor getBeanClassDescriptor() {
        return beanClassDescriptor;
    }

    /**
     * Checks whether the given class is compatible with
     * {@link ConcurrencyModel#Stateless} actors or messages, following the type
     * restrictions for them.
     *
     * @param c the class to check
     * @return true if compatible, false otherwise
     */
    private static boolean isStatelessCompatible(Class<?> c) {
        assert c != null;
        if (ImmutableHelper.isImmutableType(c)) {
            return true;
        } else if (Actor.class.isAssignableFrom(c)) {
            return true;
        } else {
            return false;
        }
    }
}
